打開你用 AI 做的 App,對著畫面按一下右鍵。跳出來的選單裡,每一項你都寫過嗎?
6 月 22 日下午 3 點 55 分,使用者 joeleee 在 usage(我做的開源小工具,顯示 Claude Code、Codex 這兩個 AI 寫程式工具用了多少額度)開了 issue(問題回報)#42。標題是簡體中文,意思是:
點了面板裡的 reload 之後,面板什麼都不顯示了。想重開,但找不到在哪裡結束。
重現步驟只有兩步:點面板裡的 reload,然後什麼都沒了。他附的截圖裡,面板什麼都沒有,只剩深灰色的底。
usage 住在 Mac 螢幕上方的選單列。點一下圖示,會彈出一個面板,顯示各個 AI 工具的額度。面板最下面有一排按鈕,「結束」也在裡面。
所以他卡住了:面板空了,「結束」按鈕跟著不見。選單列上的圖示點下去只會開面板,沒有另外的選單。當時 usage 的程式裡,會叫 App 結束的地方,只接在面板這顆按鈕上。
我翻了面板的網頁和各語言的介面文字,沒有一個字是 reload。最像的,是「結束」旁邊那顆「立即更新」。它只重抓額度數字,不會重新載入面板。
usage 的面板是一頁網頁,放在 WKWebView 裡。WKWebView 是 Mac 的 App 裡用來顯示網頁的元件,可以想成 App 裡內嵌一個小瀏覽器。它附帶一個右鍵選單:重新載入、上一頁、下一頁、在新視窗打開連結。這些不用寫一行程式就有。
面板上能把整頁重新載入的,只有這個右鍵選單。他點的,應該就是這裡。
這個選單從 5 月 20 日 v0.4.0 面板改成網頁那天就在了。從 v0.4.0 到 v0.22.4,版本號改了 78 次,每一版都帶著它。我在整個 git 紀錄裡搜 willOpenMenu、menuForEvent、contextmenu 這幾個處理右鍵選單常用的名字,第一次出現,就是修 #42 的那個 commit。在那之前,右鍵選單長什麼樣,一直是系統決定的。
usage 把面板放進 WKWebView 的方法,是把整頁 HTML 直接交給它,不給網址:
web_view.loadHTMLString_baseURL_(html, None)
最後那個 None,就是「不給網址」。這是 WKWebView 提供的正常用法,畫面照樣顯示得出來。問題在「重新載入」:瀏覽器的重新載入,是照網址再抓一次。
我寫了一支 40 行的小程式重現這件事:先把一小段 HTML 交給 WKWebView,叫一次 reload(),再把同一段 HTML 交給它一次。每一步都印出這頁的網址和畫面上的文字:
1. 載入面板:about:blank | 畫面文字:"usage\n\nClaude Code 42%"
2. reload() 之後:about:blank | 畫面文字:""
3. 重新塞入原本的 HTML:about:blank | 畫面文字:"usage\n\nClaude Code 42%"
第 1 步就是答案:畫面上明明有內容,網址卻是 about:blank,也就是瀏覽器的空白頁。第 2 步照這個網址重新載入,載回來的就是空白頁。第 3 步把 HTML 再交一次,內容才回來。

修 #42 時,我請 AI 查原因。它順便找到另一個地方,也在叫 reload。
6 月 6 日,一位外部貢獻者送來 PR #29(把改好的程式送來請我合併)。CHANGELOG(每一版改了什麼的紀錄)是這樣寫的:WKWebView 負責畫網頁的那個程式,有時會自己掛掉,App 還開著,面板卻變成一片空白或灰色,要整個 App 重開才會好。PR #29 的修法是:偵測到它掛掉,就把面板重新載入。
重新載入用的,就是 reload(),跟右鍵選單的 Reload 是同一個動作。
PR #29 附了測試,CI(每次推上去自動跑的檢查)是綠的。測試裡放的是一個假的網頁元件,它的 reload() 什麼都不做,只數自己被叫了幾次:
assert view.reloads == 1
這個測試檢查的是「有沒有叫 reload」,沒檢查「叫完之後畫面還在不在」。Day 23 也遇過一樣的事:5 月 22 日補來防止 App 找不到翻譯檔的測試,只檢查翻譯檔有沒有列進打包清單,沒檢查程式去哪裡找。兩次都是測試看的那一步對了,使用者看到的那一步沒人看。
6 月 22 日晚上,修正合併進去,程式改了兩處:
當天怎麼驗的?單元測試全綠,加上 AI 回報裡的一句:「邏輯自查:使用者右鍵點系統選單 Reload 的入口已被移除。」測試裡的右鍵選單,是測試自己做的假選單。
#42 跟著自動關閉,我回覆他,v0.22.5 隨後發出。從他開 issue 到修正合併,6 個半小時。在真的面板上按一次右鍵這件事,Day 28 會回來做。

把網頁包成 App 的做法很多,瀏覽器原本就有的功能(右鍵選單、重新整理、上一頁),常常會跟著被包進來。你的 App 只要有一塊畫面是網頁,就做一次這個檢查:
Cmd+R(重新整理的快捷鍵),再用觸控板兩指左右滑(上一頁、下一頁的手勢)。如果你的 App 跟 usage 一樣用 loadHTMLString 載入畫面、網址給 None,可以跑我那支重現程式親眼看一次。Mac 上裝了 pyobjc(讓 Python 呼叫 Mac 原生元件的套件,pip install pyobjc)就能跑:
from AppKit import NSApplication
from Foundation import NSDate, NSMakeRect, NSRunLoop
from WebKit import WKWebView, WKWebViewConfiguration
HTML = "<html><body><h1>usage</h1><p>Claude Code 42%</p></body></html>"
def spin(seconds):
NSRunLoop.currentRunLoop().runUntilDate_(NSDate.dateWithTimeIntervalSinceNow_(seconds))
def show(view, label):
result = {}
def done(value, error):
result["v"] = value if error is None else f"錯誤 {error}"
view.evaluateJavaScript_completionHandler_(
"document.URL + ' | 畫面文字:' + JSON.stringify(document.body ? document.body.innerText : '')",
done,
)
spin(1)
print(f"{label}:{result.get('v')}")
NSApplication.sharedApplication()
view = WKWebView.alloc().initWithFrame_configuration_(
NSMakeRect(0, 0, 364, 400), WKWebViewConfiguration.alloc().init()
)
view.loadHTMLString_baseURL_(HTML, None)
spin(2)
show(view, "1. 載入面板")
view.reload()
spin(2)
show(view, "2. reload() 之後")
view.loadHTMLString_baseURL_(HTML, None)
spin(2)
show(view, "3. 重新塞入原本的 HTML")
第 2 步的空白,跟 HTML 寫了什麼無關。我把 usage 修正前真正的面板 HTML 放進去再跑一次,第 2 步一樣是空的。只要網址給 None,reload 載回來的就是空白頁。
工作單(交給 AI 的任務說明)裡那格「做完怎麼算對」,今天再加兩行(沒用過這張工作單,也可以直接把這兩行交給 AI,當作驗收要求):
做完怎麼算對:
- (Day 4、Day 8 到 Day 25 加的幾行)
- 畫面是網頁的 App:在打包好的 App 上,對每個畫面按右鍵、把選單每一項點一次,再按 Cmd+R、兩指左右滑;每一步貼出畫面截圖。程式自己會重新載入畫面的地方,要在真的網頁元件上跑一次,貼出載入後畫面上的文字;只用假物件數它被叫了幾次,不算數
- 結束、重開這類出口:列出 App 裡每一個能結束它的地方,各在哪個畫面上。全部在同一個畫面上的,寫出那個畫面壞掉時使用者怎麼結束 App
AI 寫進 App 的元件,會帶來你沒寫過的功能。你沒寫過,就很難想到去測;使用者不知道哪些是你寫的,看到就會點。
會叫 reload 的,不只右鍵選單。程式裡那個 reload(),是 PR #29 送來的;送的人,是 Day 1 開場那位 ericweichun:usage 曾把一天的 Codex 花費算成 12 倍,就是他修好的。他送的 27 個 PR,我全部合併了,每一個都附了測試。修 12 倍那次(PR #40)附的測試,守住了那個錯;PR #29 附的測試,只數了 reload 被叫幾次。同一個人送的,CI 都是綠的。我不靠讀 code 把關,怎麼分得出這兩種測試?明天看。
loadHTMLString(_:baseURL:) 說明:https://developer.apple.com/documentation/webkit/wkwebview/loadhtmlstring(_:baseurl:)